<?php

/**
 * km::util::Pager class
 * 
 * @author vbolshov
 */

namespace km\util;

use km\db\Select;
use \IteratorAggregate;
use \ArrayObject;

class Pager implements IteratorAggregate {
	/**
	 * @var int
	 */
	private $row_count, $page_size, $page_number, $page_count;
	/**
	 * @var string Format for use in sprintf ( like /articles/page/%d/ )
	 */
	private $url_fmt, $first_page_fmt;
	/**
	 * @var Closure
	 */
	private $ranger;
	/**
	 * Whether to append QUERY_STRING parameters
	 * @var bool
	 */
	private $appendQueryString = true,
		$appendQueryStringExceptions = array();
	/**
	 * @param int | Select $rows
	 */
	function __construct($rows, $page_number, $page_size = 20)
	{
		$this->page_number = ($page_number > 1) ? $page_number : 1;
		$this->page_size = $page_size;
		
		if ($rows instanceof Select)
		{
			$this->row_count = $rows->count();
			$rows->paginate($this->page_number, $this->page_size);
		} else {
			$this->row_count = $rows;
		}
		
		$this->page_count = ceil($this->row_count / $this->page_size);
	}
	/**
	 * Automagically append QUERY_STRING params to page URLs.
	 * You may optionally exclude some of the GET parameters with
	 * setAppendQueryStringException()
	 * @param bool $flag
	 */
	function setAppendQueryString($flag)
	{
		$this->appendQueryString = $flag;
	}
	/**
	 * Exclude some of the GET parameters appended to QUERY_STRING
	 * @param string[] $keys
	 */
	function setAppendQueryStringException(array $keys)
	{
		$this->appendQueryStringExceptions = array_flip($keys);
	}
	/**
	 * Exclude some of the GET parameters appended to QUERY_STRING
	 * @param string[] $keys
	 */
	function addAppendQueryStringException(array $keys)
	{
		$this->appendQueryStringExceptions = array_merge($this->appendQueryStringExceptions, array_flip($keys));
	}
	/**
	 * @param string $url 
	 * @return string
	 */
	protected function appendQueryString($url)
	{
		if (! $this->appendQueryString) {
			return $url;
		} else {
			if (count($appendArray = array_diff_key($_GET, $this->appendQueryStringExceptions))) {
				$append = http_build_query($appendArray);
				if (substr_count($url, '?')) {
					return $url . '&' . $append;
				} else {
					return $url . '?' . $append;
				}
			} else {
				return $url;
			}
		}
	}
	/**
	 * @return int
	 */
	static function resolvePageNumber($key = 'page', $input = INPUT_GET)
	{
		if (! $page = filter_input($input, $key, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1)))) {
			$page = 1;
		}
		return $page;
	}
	/**
	 * @param string $fmt
	 */
	function setUrlFormat($fmt, $first_page_fmt = null)
	{
		$this->url_fmt = $this->appendQueryString($fmt);
		$this->first_page_fmt = $this->appendQueryString($first_page_fmt ?: $fmt);
	}
	/**
	 * @return string
	 */
	function nextPageUrl()
	{
		return $this->url($this->page_number + 1);
	}
	/**
	 * @return string
	 */
	function prevPageUrl()
	{
		return $this->url($this->page_number - 1);
	}
	/**
	 * Set ranger, that determines pang-ranging policy
	 *
	 * @param string | Closure $ranger
	 * @param array $options
	 */
	function setRanger($ranger, $options = null)
	{
		if ($ranger instanceof Closure)
		{
			$this->ranger = $ranger;
		} else {
			$this->ranger = $this->getBuiltInRanger($ranger, $options);
		}
	}
	/**
	 * @return bool
	 */
	function isFirst()
	{
		return 1 >= $this->page_number;
	}
	/**
	 * @return bool
	 */
	function isLast()
	{
		return $this->page_count <= $this->page_number;
	}
	/**
	 * @return int
	 */
	function getPreviousPage()
	{
		return $this->page_number - 1;
	}
	/**
	 * @return int
	 */
	function getNextPage()
	{
		return $this->page_number + 1;
	}
	/**
	 * @return int
	 */
	function getFirstPage()
	{
		return min($this->getPages());
	}
	/**
	 * @return int
	 */
	function getLastPage()
	{
		return max($this->getPages());
	}
	/**
	 * @return bool
	 */
	function isSelected($page_number)
	{
		return $page_number == $this->page_number;
	}
	/**
	 * @return int
	 */
	function getRowCount()
	{
		return $this->row_count;
	}
	/**
	 * @return int
	 */
	function getPageCount()
	{
		return $this->page_count;
	}
	/**
	 * @return int
	 */
	function getPageNumber()
	{
		return $this->page_number;
	}
	/**
	 * @return int
	 */
	function getPageSize()
	{
		return $this->page_size;
	}
	/**
	 * @return ArrayObject
	 */
	function getIterator()
	{
		$arr = array();
		foreach ($this->getPages() as $page_number)
		{
			$arr[] = array(
				'number' => $page_number, 
				'url' => $this->url($page_number), 
				'selected' => $this->isSelected($page_number)
			);
		}
		return new ArrayObject($arr);
	}
	/**
	 * @param int $page_number
	 * @return string
	 */
	function url($page_number)
	{
		return sprintf($this->getUrlFormat($page_number), $page_number);
	}
	
	/**
	 * @return array (page_num1, page_num2,..)
	 */
	function getPages()
	{
		return $this->getRanger()->__invoke($this->page_count, $this->page_number);
	}
	
	private function getRanger()
	{
		return $this->ranger ?: $this->getBuiltInRanger();
	}
	
	function plainRanger($page_count, $page_number)
	{
		return $this->getPageArray(1, $page_count);
	}
	
	function windowingRanger($page_count, $page_number, $options)
	{
		assert(is_array($options));
		assert(isset($options['size']));
		assert($options['size'] > 0);
		
		$half_size = floor($options['size'] / 2);
		if ($page_number <= $half_size) {
			$min = 1;
			$max = $options['size'];
		} elseif (($page_count - $page_number) < $half_size) {
			$min = $page_count - $options['size'] + 1;
			$max = $page_count;
		} elseif ($options['size'] % 2) {
			$min = $page_number - $half_size;
			$max = $page_number + $half_size;
		} else {
			$min = $page_number - $half_size + 1;
			$max = $page_number + $half_size;
		}
		return $this->getPageArray($min, $max);
	}
	
	private function getDefaultUrlFormat()
	{
		if (strlen($query = http_build_query($_GET, null, '&amp;')))
		{
			return '?' . $query . '&amp;page=%d';
		} else {
			return '?page=%d'; 
		}
	}
	/**
	 * @param string $type
	 * @param array $options
	 * @return Closure
	 */
	private function getBuiltInRanger($type = 'plain', $options = null)
	{
		$_this = $this;
		return function($page_count, $page_number) use ($type, $options, $_this) {
			$method = $type . 'Ranger';
			return $_this->$method($page_count, $page_number, $options);
		};
	}
	
	private function getPageArray($min, $max)
	{
		$min = (int) max($min, 1);
		$max = (int) min($max, $this->page_count);
		return range($min, $max);
	}
	
	function getUrlFormat($page_number = null)
	{
		$fmt = (1 == $page_number) ? $this->first_page_fmt : $this->url_fmt;
		return $fmt ?: $this->getDefaultUrlFormat();
	}
}